Skip to main content

copp\copp\copp2\dp2/
reach_set2.rs

1//! Reachable-set construction for second-order path parameterization.
2//!
3//! # Method identity
4//! This module implements the reachable-set stage via **Reachability Analysis (RA)** in
5//! a **Dynamic Programming (DP)** compatible form, which can be used by:
6//! - **Time-Optimal Path Parameterization (TOPP2)**,
7//! - **Convex-Objective Path Parameterization (COPP2)**.
8//!
9//! # Discrete variables (local notation)
10//! On a path grid `s[0..=n]`:
11//! - `a[k]` denotes $\dot{s}_k^2$ (nonnegative scalar state);
12//! - reachable interval at station `k` is `[a_min[k], a_max[k]]`.
13//!
14//! # High-level pipeline
15//! 1. Validate boundary feasibility at both interval ends.
16//! 2. Backward pass from terminal boundary to construct feasible intervals.
17//! 3. Optional forward clipping (when bidirectional mode is enabled) to enforce start boundary.
18//! 4. Return interval arrays `a_min` / `a_max` for downstream solvers.
19
20use crate::copp::copp2::formulation::Topp2Problem;
21use crate::copp::{ApproxOrdering, approx_order};
22use crate::diag::{
23    CoppError, DebugVerboser, SilentVerboser, SummaryVerboser, TraceVerboser, Verboser, Verbosity,
24    check_abs_rel_tol, check_strictly_positive, format_duration_human,
25};
26use crate::math::numerical::{
27    LP_BOUND, Lp2dWarmStart, LpToleranceOptions, lp_1d, lp_2d_incre_max_y, normalize_lp2d,
28};
29use core::f64;
30
31/// Reachable intervals of $a(s)=\dot{s}^2$ for TOPP2/COPP2.  
32/// For each point `s[k]`, the reachable set is `a_min[k] <= a[k] <= a_max[k]`.
33pub struct ReachSet2 {
34    pub a_max: Vec<f64>,
35    pub a_min: Vec<f64>,
36}
37
38/// Compute backward-only reachable intervals of $a(s)=\dot{s}^2$ using Reachability Analysis.
39///
40/// Performs a single backward pass from the terminal boundary; the start boundary
41/// is **not** enforced.  Use this when only the terminal state is constrained, or
42/// as an intermediate step before bidirectional analysis.
43///
44/// # Returns
45/// [`ReachSet2`] with `a_min[k] <= a[k] <= a_max[k]` for every station.
46///
47/// # Errors
48/// Returns `CoppError` when boundary states are infeasible, LP subproblems fail,
49/// or numerical comparisons violate configured tolerances.
50///
51/// # Contract
52/// - `problem` indices and boundaries must be consistent with the constraints domain.
53/// - `options` tolerances must be positive and numerically meaningful.
54#[inline]
55pub fn reach_set2_backward(
56    problem: &Topp2Problem,
57    options: &ReachSet2Options,
58) -> Result<ReachSet2, CoppError> {
59    match options.verbosity {
60        Verbosity::Silent => reach_set2_core::<false>(problem, (options, SilentVerboser)),
61        Verbosity::Summary => reach_set2_core::<false>(problem, (options, SummaryVerboser::new())),
62        Verbosity::Debug => reach_set2_core::<false>(problem, (options, DebugVerboser::new())),
63        Verbosity::Trace => reach_set2_core::<false>(problem, (options, TraceVerboser::new())),
64    }
65}
66
67/// Compute bidirectional reachable intervals of $a(s)=\dot{s}^2$.
68///
69/// Performs a backward pass then clips the result with a forward pass to enforce
70/// **both** the start and terminal boundary constraints simultaneously.
71///
72/// # Returns
73/// [`ReachSet2`] with `a_min[k] <= a[k] <= a_max[k]` for every station.
74///
75/// # Errors
76/// Returns `CoppError` when boundary states are infeasible, LP subproblems fail,
77/// or numerical comparisons violate configured tolerances.
78///
79/// # Contract
80/// - `problem` indices and boundaries must be consistent with the constraints domain.
81/// - `options` tolerances must be positive and numerically meaningful.
82#[inline]
83pub fn reach_set2_bidirectional(
84    problem: &Topp2Problem,
85    options: &ReachSet2Options,
86) -> Result<ReachSet2, CoppError> {
87    match options.verbosity {
88        Verbosity::Silent => reach_set2_core::<true>(problem, (options, SilentVerboser)),
89        Verbosity::Summary => reach_set2_core::<true>(problem, (options, SummaryVerboser::new())),
90        Verbosity::Debug => reach_set2_core::<true>(problem, (options, DebugVerboser::new())),
91        Verbosity::Trace => reach_set2_core::<true>(problem, (options, TraceVerboser::new())),
92    }
93}
94
95/// Core RA implementation with layered verbosity.
96/// # Mode
97/// - `BIDIRECTION = false`: backward reachable set only (terminal boundary constrained).
98/// - `BIDIRECTION = true`: bidirectional reachable set (both start and terminal boundaries constrained).
99fn reach_set2_core<const BIDIRECTION: bool>(
100    problem: &Topp2Problem,
101    options_verboser: (&ReachSet2Options, impl Verboser),
102) -> Result<ReachSet2, CoppError> {
103    let (options, mut verboser) = options_verboser;
104    if verboser.is_enabled(Verbosity::Summary) {
105        verboser.record_start_time();
106        crate::verbosity_log!(
107            crate::diag::Verbosity::Summary,
108            "reach_set2 started: {} <= idx_s <= {}, a_start = {}, a_final = {}.",
109            problem.idx_s_interval.0,
110            problem.idx_s_interval.1,
111            problem.a_boundary.0,
112            problem.a_boundary.1,
113        );
114        if BIDIRECTION {
115            crate::verbosity_log!(
116                crate::diag::Verbosity::Summary,
117                "Bidirectional reachable set will be computed."
118            );
119        } else {
120            crate::verbosity_log!(
121                crate::diag::Verbosity::Summary,
122                "Backward reachable set will be computed."
123            );
124        }
125    }
126
127    let (idx_s_start, idx_s_final) = problem.idx_s_interval;
128    let a_max_0 = problem.constraints.amax_unchecked(problem.idx_s_interval.0);
129    if matches!(
130        approx_order(
131            problem.a_boundary.0,
132            a_max_0,
133            options.a_cmp_abs_tol,
134            options.a_cmp_rel_tol,
135        ),
136        ApproxOrdering::Greater
137    ) {
138        let err = CoppError::Infeasible(
139            "reach_set2".into(),
140            format!(
141                "The initial state a_start = {} cannot be greater than the maximum feasible state a_max[0] = {a_max_0} at idx_s_start = {idx_s_start}",
142                problem.a_boundary.0,
143            ),
144        );
145        if verboser.is_enabled(Verbosity::Debug) {
146            crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
147        } else if verboser.is_enabled(Verbosity::Summary) {
148            crate::verbosity_log!(
149                crate::diag::Verbosity::Debug,
150                "reach_set2: the backward pass failed at index {idx_s_start} due to infeasibility of the initial state."
151            );
152        }
153        return Err(err);
154    }
155    let a_max_f = problem.constraints.amax_unchecked(problem.idx_s_interval.1);
156    if matches!(
157        approx_order(
158            problem.a_boundary.1,
159            a_max_f,
160            options.a_cmp_abs_tol,
161            options.a_cmp_rel_tol,
162        ),
163        ApproxOrdering::Greater
164    ) {
165        let err = CoppError::Infeasible(
166            "reach_set2".into(),
167            format!(
168                "The final state a_final = {} cannot be greater than the maximum feasible state a_max[n] = {a_max_f} at idx_s_final = {idx_s_final}",
169                problem.a_boundary.1,
170            ),
171        );
172        if verboser.is_enabled(Verbosity::Debug) {
173            crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
174        } else if verboser.is_enabled(Verbosity::Summary) {
175            crate::verbosity_log!(
176                crate::diag::Verbosity::Debug,
177                "reach_set2: the backward pass failed at index {idx_s_final} due to infeasibility of the final state."
178            );
179        }
180        return Err(err);
181    }
182
183    // Step 1. Initialize a_max and a_min at s_final
184    let n = idx_s_final - idx_s_start;
185    let mut a_max = vec![f64::INFINITY; n + 1];
186    let mut a_min = vec![0.0; n + 1];
187    *a_max.last_mut().unwrap() = problem.a_boundary.1;
188    *a_min.last_mut().unwrap() = problem.a_boundary.1;
189    // Step 2. Backward pass
190    if verboser.is_enabled(Verbosity::Debug) {
191        crate::verbosity_log!(crate::diag::Verbosity::Summary, "Backward pass started.");
192    }
193
194    let mut a_b = Vec::<(f64, f64, f64)>::with_capacity(2 + 2 * problem.constraints.acc_rows());
195    let mut a_max_next = problem.a_boundary.1;
196    let mut a_min_next = problem.a_boundary.1;
197    for (k, (a_max_k, a_min_k)) in a_max
198        .iter_mut()
199        .zip(a_min.iter_mut())
200        .take(n)
201        .enumerate()
202        .rev()
203    {
204        let idx_s = idx_s_start + k;
205        if verboser.is_enabled(Verbosity::Trace) {
206            crate::verbosity_log!(
207                crate::diag::Verbosity::Summary,
208                "\tBackward pass at k = {k} (idx_s = {idx_s}) to compute a[k]: {a_min_next} <= a[k+1] <= {a_max_next}."
209            );
210        }
211
212        a_b.clear();
213        a_b.push((1.0, 0.0, a_max_next));
214        a_b.push((-1.0, 0.0, -a_min_next));
215        problem.constraints.fill_acc_topp2::<true>(&mut a_b, idx_s);
216        // a_b.0 * a[k+1] + a_b.1 * a[k] <= a_b.2
217
218        let a_next_mid = 0.5 * (a_max_next + a_min_next);
219        let (a_max_curr, a_min_curr) = match approx_order(
220            a_max_next,
221            a_min_next,
222            options.a_cmp_abs_tol,
223            options.a_cmp_rel_tol,
224        ) {
225            ApproxOrdering::Equal => {
226                if verboser.is_enabled(Verbosity::Trace) {
227                    crate::verbosity_log!(
228                        crate::diag::Verbosity::Summary,
229                        "\t\ta[k+1] = {a_next_mid}"
230                    );
231                }
232                lp_1d::<true>(
233                    a_b.iter().skip(2).map(|&coeffs| {
234                        // coeffs.0 * a_next  + coeffs.1* a_curr <= coeffs.2
235                        // coeffs.1 * a_curr <= coeffs.2 - coeffs.0 * a_next
236                        (coeffs.1, coeffs.2 - coeffs.0 * a_next_mid)
237                    }),
238                    &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
239                )
240            }
241            ApproxOrdering::Greater => {
242                if verboser.is_enabled(Verbosity::Trace) {
243                    crate::verbosity_log!(
244                        crate::diag::Verbosity::Summary,
245                        "\t\t{a_min_next} <= a[k+1] <= {a_max_next}"
246                    );
247                }
248                // Check whether the forward pass of `amin_curr` can be skipped
249                let (a_test_max, a_test_min) = lp_1d::<true>(
250                    a_b.iter().skip(2).map(|&coeffs| {
251                        // coeffs.0 * a_next  + coeffs.1* a_curr <= coeffs.2
252                        // coeffs.0 * a_next <= coeffs.2 - coeffs.1 * a_curr
253                        (coeffs.0, coeffs.2 - coeffs.1 * *a_min_k)
254                    }),
255                    &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
256                );
257                let flag_need_min = a_test_max.is_nan()
258                    || a_test_min.is_nan()
259                    || a_test_max < a_min_next
260                    || a_test_max > a_max_next;
261                if verboser.is_enabled(Verbosity::Trace) {
262                    crate::verbosity_log!(
263                        crate::diag::Verbosity::Summary,
264                        "\t\tBackward skip checks at idx_s = {idx_s}: need_max = true, need_min = {flag_need_min}."
265                    );
266                }
267                if flag_need_min {
268                    backward_bound_a_next::<true, true>(&mut a_b, a_next_mid, options.lp_feas_tol)
269                } else {
270                    (
271                        backward_bound_a_next::<true, false>(
272                            &mut a_b,
273                            a_next_mid,
274                            options.lp_feas_tol,
275                        )
276                        .0,
277                        *a_min_k,
278                    )
279                }
280            }
281            ApproxOrdering::Less => {
282                let err = CoppError::Infeasible(
283                    "reach_set2".into(),
284                    format!(
285                        "The reachable set is empty at index {idx_s} during the backward pass where a_max_next = {a_max_next}, a_min_next = {a_min_next}"
286                    ),
287                );
288                if verboser.is_enabled(Verbosity::Debug) {
289                    crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
290                } else if verboser.is_enabled(Verbosity::Summary) {
291                    crate::verbosity_log!(
292                        crate::diag::Verbosity::Debug,
293                        "reach_set2: the backward pass failed at index {idx_s} due to infeasibility."
294                    );
295                }
296                return Err(err);
297            }
298        };
299
300        if a_max_curr.is_nan() || a_min_curr.is_nan() {
301            let err = CoppError::Infeasible(
302                "reach_set2".into(),
303                format!(
304                    "The reachable set is empty at index {idx_s} during the backward pass where a_max = {a_max_curr}, a_min = {a_min_curr}"
305                ),
306            );
307            if verboser.is_enabled(Verbosity::Debug) {
308                crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
309            } else if verboser.is_enabled(Verbosity::Summary) {
310                crate::verbosity_log!(
311                    crate::diag::Verbosity::Debug,
312                    "reach_set2: the backward pass failed at index {idx_s} due to infeasibility."
313                );
314            }
315            return Err(err);
316        }
317        if verboser.is_enabled(Verbosity::Trace) {
318            crate::verbosity_log!(
319                crate::diag::Verbosity::Summary,
320                "\t\tBackward LP result at k = {k}: {a_min_curr} <= a[k] <= {a_max_curr}."
321            );
322        }
323
324        *a_max_k = a_max_curr.min(problem.constraints.amax_unchecked(idx_s));
325        *a_min_k = a_min_curr.max(0.0);
326
327        if verboser.is_enabled(Verbosity::Trace) {
328            crate::verbosity_log!(
329                crate::diag::Verbosity::Summary,
330                "\t\tAfter clipping with path constraints at k = {k}: {} <= a[k] <= {}.",
331                *a_min_k,
332                *a_max_k
333            );
334        }
335
336        match approx_order(
337            *a_max_k,
338            *a_min_k,
339            options.a_cmp_abs_tol,
340            options.a_cmp_rel_tol,
341        ) {
342            ApproxOrdering::Less => {
343                let err = CoppError::Infeasible(
344                    "reach_set2".into(),
345                    format!(
346                        "The reachable set is empty at k = {k} (idx_s = {idx_s}) during the backward pass where a_max = {a_max_k}, a_min = {a_min_k}"
347                    ),
348                );
349                if verboser.is_enabled(Verbosity::Debug) {
350                    crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
351                } else if verboser.is_enabled(Verbosity::Summary) {
352                    crate::verbosity_log!(
353                        crate::diag::Verbosity::Debug,
354                        "reach_set2: the backward pass failed at k = {k} (idx_s = {idx_s}) due to infeasibility."
355                    );
356                }
357                return Err(err);
358            }
359            ApproxOrdering::Equal => {
360                if verboser.is_enabled(Verbosity::Debug) && k < n - 1 {
361                    crate::verbosity_log!(
362                        crate::diag::Verbosity::Summary,
363                        "The backward reachable set at k = {k} (idx_s = {idx_s}) is degenerate since a_max = {a_max_k} and a_min = {a_min_k} are approximately equal."
364                    );
365                }
366                *a_max_k = 0.5 * (*a_max_k + *a_min_k);
367                *a_min_k = *a_max_k;
368            }
369            ApproxOrdering::Greater => {}
370        }
371        a_max_next = *a_max_k;
372        a_min_next = *a_min_k;
373
374        if verboser.is_enabled(Verbosity::Trace) {
375            crate::verbosity_log!(
376                crate::diag::Verbosity::Summary,
377                "\t\tBackward propagated interval to k-1: {a_min_next} <= a[k] <= {a_max_next}."
378            );
379        }
380    }
381
382    if BIDIRECTION {
383        if verboser.is_enabled(Verbosity::Debug) {
384            crate::verbosity_log!(crate::diag::Verbosity::Summary, "Forward pass started.");
385        }
386
387        *a_max.first_mut().unwrap() = problem.a_boundary.0;
388        *a_min.first_mut().unwrap() = problem.a_boundary.0;
389        let mut a_max_prev = problem.a_boundary.0;
390        let mut a_min_prev = problem.a_boundary.0;
391        for (k, (a_max_k, a_min_k)) in a_max.iter_mut().zip(a_min.iter_mut()).enumerate().skip(1) {
392            let idx_s = idx_s_start + k;
393            if verboser.is_enabled(Verbosity::Trace) {
394                crate::verbosity_log!(
395                    crate::diag::Verbosity::Summary,
396                    "\tForward pass at k = {k} (idx_s = {idx_s}): prev interval {} <= a[k-1] <= {}, backward interval {} <= a[k] <= {}.",
397                    a_min_prev,
398                    a_max_prev,
399                    *a_min_k,
400                    *a_max_k
401                );
402            }
403
404            a_b.clear();
405            a_b.push((1.0, 0.0, a_max_prev));
406            a_b.push((-1.0, 0.0, -a_min_prev));
407            problem
408                .constraints
409                .fill_acc_topp2::<false>(&mut a_b, idx_s_start + k - 1);
410            // a_b.0 * a[k-1] + a_b.1 * a[k] <= a_b.2
411
412            let a_prev_mid = 0.5 * (a_max_prev + a_min_prev);
413            let (a_max_curr, a_min_curr) = match approx_order(
414                a_max_prev,
415                a_min_prev,
416                options.a_cmp_abs_tol,
417                options.a_cmp_rel_tol,
418            ) {
419                ApproxOrdering::Equal => {
420                    lp_1d::<true>(
421                        a_b.iter().skip(2).map(|&coeffs| {
422                            // coeffs.0 * a_prev  + coeffs.1* a_curr <= coeffs.2
423                            // coeffs.1 * a_curr <= coeffs.2 - coeffs.0 * a_prev
424                            (coeffs.1, coeffs.2 - coeffs.0 * a_prev_mid)
425                        }),
426                        &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
427                    )
428                }
429                ApproxOrdering::Greater => {
430                    // Check whether the forward pass of `amax_curr` can be skipped
431                    let (a_test_max, a_test_min) = lp_1d::<true>(
432                        a_b.iter().skip(2).map(|&coeffs| {
433                            // coeffs.0 * a_prev  + coeffs.1* a_curr <= coeffs.2
434                            // coeffs.0 * a_prev <= coeffs.2 - coeffs.1 * a_curr
435                            (coeffs.0, coeffs.2 - coeffs.1 * *a_max_k)
436                        }),
437                        &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
438                    );
439                    let flag_need_max = a_test_max.is_nan()
440                        || a_test_min.is_nan()
441                        || a_test_max < a_min_prev
442                        || a_test_max > a_max_prev;
443                    // Check whether the forward pass of `amin_curr` can be skipped
444                    let (a_test_max, a_test_min) = lp_1d::<true>(
445                        a_b.iter().skip(2).map(|&coeffs| {
446                            // coeffs.0 * a_prev  + coeffs.1* a_curr <= coeffs.2
447                            // coeffs.0 * a_prev <= coeffs.2 - coeffs.1 * a_curr
448                            (coeffs.0, coeffs.2 - coeffs.1 * *a_min_k)
449                        }),
450                        &LpToleranceOptions::with_feas_tol(options.lp_feas_tol),
451                    );
452                    let flag_need_min = a_test_max.is_nan()
453                        || a_test_min.is_nan()
454                        || a_test_max < a_min_prev
455                        || a_test_max > a_max_prev;
456
457                    if verboser.is_enabled(Verbosity::Trace) {
458                        crate::verbosity_log!(
459                            crate::diag::Verbosity::Summary,
460                            "\t\tForward skip checks at idx_s = {idx_s}: need_max = {flag_need_max}, need_min = {flag_need_min}."
461                        );
462                    }
463
464                    // forward and backward is the same
465                    match (flag_need_max, flag_need_min) {
466                        (true, true) => backward_bound_a_next::<true, true>(
467                            &mut a_b,
468                            a_prev_mid,
469                            options.lp_feas_tol,
470                        ),
471                        (true, false) => (
472                            backward_bound_a_next::<true, false>(
473                                &mut a_b,
474                                a_prev_mid,
475                                options.lp_feas_tol,
476                            )
477                            .0,
478                            *a_min_k,
479                        ),
480                        (false, true) => (
481                            *a_max_k,
482                            backward_bound_a_next::<false, true>(
483                                &mut a_b,
484                                a_prev_mid,
485                                options.lp_feas_tol,
486                            )
487                            .1,
488                        ),
489                        (false, false) => (*a_max_k, *a_min_k),
490                    }
491                }
492                ApproxOrdering::Less => {
493                    let err = CoppError::Infeasible(
494                        "reach_set2".into(),
495                        format!(
496                            "The reachable set is empty at index {idx_s} during the forward pass where a_max_prev = {a_max_prev}, a_min_prev = {a_min_prev}"
497                        ),
498                    );
499                    if verboser.is_enabled(Verbosity::Debug) {
500                        crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
501                    } else if verboser.is_enabled(Verbosity::Summary) {
502                        crate::verbosity_log!(
503                            crate::diag::Verbosity::Debug,
504                            "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
505                        );
506                    }
507                    return Err(err);
508                }
509            };
510
511            if verboser.is_enabled(Verbosity::Trace) {
512                crate::verbosity_log!(
513                    crate::diag::Verbosity::Summary,
514                    "\t\tForward LP result at idx_s = {idx_s}: {a_min_curr} <= a[k] <= {a_max_curr}."
515                );
516            }
517
518            if a_max_curr.is_nan() || a_min_curr.is_nan() {
519                let err = CoppError::Infeasible(
520                    "reach_set2".into(),
521                    format!(
522                        "The reachable set is empty at index {idx_s} during the forward pass where a_max = {a_max_curr}, a_min = {a_min_curr}"
523                    ),
524                );
525                if verboser.is_enabled(Verbosity::Debug) {
526                    crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
527                } else if verboser.is_enabled(Verbosity::Summary) {
528                    crate::verbosity_log!(
529                        crate::diag::Verbosity::Debug,
530                        "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
531                    );
532                }
533                return Err(err);
534            }
535
536            a_max_prev = a_max_curr.min(*a_max_k);
537            a_min_prev = a_min_curr.max(*a_min_k);
538            if verboser.is_enabled(Verbosity::Trace) {
539                crate::verbosity_log!(
540                    crate::diag::Verbosity::Summary,
541                    "\t\tAfter intersecting with backward interval at idx_s = {idx_s}: {a_min_prev} <= a[k] <= {a_max_prev}."
542                );
543            }
544            match approx_order(
545                a_max_prev,
546                a_min_prev,
547                options.a_cmp_abs_tol,
548                options.a_cmp_rel_tol,
549            ) {
550                ApproxOrdering::Less => {
551                    let err = CoppError::Infeasible(
552                        "reach_set2".into(),
553                        format!(
554                            "The reachable set is empty at index {idx_s} during the forward pass where a_max = {a_max_prev}, a_min = {a_min_prev}"
555                        ),
556                    );
557                    if verboser.is_enabled(Verbosity::Debug) {
558                        crate::verbosity_log!(crate::diag::Verbosity::Summary, "{err:?}");
559                    } else if verboser.is_enabled(Verbosity::Summary) {
560                        crate::verbosity_log!(
561                            crate::diag::Verbosity::Debug,
562                            "reach_set2: the forward pass failed at index {idx_s} due to infeasibility."
563                        );
564                    }
565                    return Err(err);
566                }
567                ApproxOrdering::Equal => {
568                    if verboser.is_enabled(Verbosity::Debug) && k < n - 1 {
569                        crate::verbosity_log!(
570                            crate::diag::Verbosity::Summary,
571                            "The forward reachable set at idx_s = {idx_s} is degenerate since a_max_prev and a_min_prev are approximately equal at {a_prev_mid}."
572                        );
573                    }
574                    a_max_prev = 0.5 * (a_max_prev + a_min_prev);
575                    a_min_prev = a_max_prev;
576                }
577                ApproxOrdering::Greater => {}
578            }
579            if verboser.is_enabled(Verbosity::Trace) {
580                crate::verbosity_log!(
581                    crate::diag::Verbosity::Summary,
582                    "\t\tForward propagated interval to next step: {a_min_prev} <= a[k] <= {a_max_prev}."
583                );
584            }
585            *a_max_k = a_max_prev;
586            *a_min_k = a_min_prev;
587        }
588    }
589
590    if verboser.is_enabled(Verbosity::Summary) {
591        crate::verbosity_log!(
592            crate::diag::Verbosity::Summary,
593            "reach_set2: {}backward total elapsed time = {}.",
594            if BIDIRECTION { "forward + " } else { "" },
595            format_duration_human(verboser.elapsed())
596        );
597    }
598
599    Ok(ReachSet2 { a_max, a_min })
600}
601
602/// Backward propagation to compute feasible `a[k]` bounds at the current step given the next step's `a[k+1]=a_next` bounds.  
603/// a_b.0 * a[k+1] + a_b.1 * a[k] <= a_b.2
604/// Returns (a_max_curr, a_min_curr)
605fn backward_bound_a_next<const MAX: bool, const MIN: bool>(
606    a_b: &mut [(f64, f64, f64)],
607    a_next_mid: f64,
608    lp_fea_tol: f64,
609) -> (f64, f64) {
610    let warm_start = Lp2dWarmStart {
611        x0: (a_next_mid, LP_BOUND),
612        skip: 2,
613    };
614
615    normalize_lp2d(a_b);
616    let a_curr_max = if MAX {
617        let (_, a_curr_max) = lp_2d_incre_max_y::<_, false>(
618            a_b,
619            &warm_start,
620            &LpToleranceOptions::with_feas_tol(lp_fea_tol),
621        );
622        a_curr_max
623    } else {
624        f64::INFINITY
625    };
626
627    let a_curr_min = if MIN {
628        // Better than transform the sign in the lp_2d_incre function.
629        a_b.iter_mut().for_each(|(_, b, _)| {
630            *b = -*b;
631        });
632        let (_, a_curr_min_neg) = lp_2d_incre_max_y::<_, false>(
633            a_b,
634            &warm_start,
635            &LpToleranceOptions::with_feas_tol(lp_fea_tol),
636        );
637        -(a_curr_min_neg.min(0.0))
638    } else {
639        0.0
640    };
641
642    (a_curr_max, a_curr_min)
643}
644
645/// Builder for `ReachSet2Options`.
646pub struct ReachSet2OptionsBuilder {
647    /// Feasibility tolerance used by LP subproblems in reachable-set computation.
648    pub lp_feas_tol: f64,
649    /// Absolute tolerance for comparing interval bounds `a_max` and `a_min`.
650    pub a_cmp_abs_tol: f64,
651    /// Relative tolerance for comparing interval bounds `a_max` and `a_min`.
652    pub a_cmp_rel_tol: f64,
653    /// Verbosity level for diagnostics during reachability analysis.
654    pub verbosity: Verbosity,
655}
656
657impl Default for ReachSet2OptionsBuilder {
658    #[inline]
659    fn default() -> Self {
660        Self {
661            lp_feas_tol: 1e-8,
662            a_cmp_abs_tol: 1e-8,
663            a_cmp_rel_tol: 1e-8,
664            verbosity: Verbosity::default(),
665        }
666    }
667}
668
669impl ReachSet2OptionsBuilder {
670    /// Create a new `ReachSet2OptionsBuilder` with default values.
671    pub fn new() -> Self {
672        Default::default()
673    }
674
675    /// Set the tolerance for checking the feasibility of the linear program.  
676    /// The default value is 1E-8.
677    pub fn lp_feas_tol(mut self, tol: f64) -> Self {
678        self.lp_feas_tol = tol;
679        self
680    }
681
682    /// Set the absolute tolerance for comparing `a_max` and `a_min` to determine whether the reachable set is empty (`a_max < a_min`) or degenerated (`a_max == a_min`).  
683    /// Let `tol = max(a_cmp_abs_tol, a_cmp_rel_tol * max(|a_max|, |a_min|))`.  
684    /// + If `a_max < a_min - tol`, then the reachable set is empty.  
685    /// + If `a_max > a_min + tol`, then the reachable set is non-degenerated.  
686    /// + Otherwise, the reachable set is degenerated into a single point.
687    ///
688    /// The default value is 1E-8.
689    #[inline]
690    pub fn a_cmp_abs_tol(mut self, tol: f64) -> Self {
691        self.a_cmp_abs_tol = tol;
692        self
693    }
694
695    /// Set the relative tolerance for comparing `a_max` and `a_min`. More details refer to `a_cmp_abs_tol`.
696    /// The default value is 1E-8.
697    #[inline]
698    pub fn a_cmp_rel_tol(mut self, tol: f64) -> Self {
699        self.a_cmp_rel_tol = tol;
700        self
701    }
702
703    /// Set the verbosity level for logging. More details refer to `Verbosity`.  
704    /// The default value is `Verbosity::Silent`.
705    #[inline]
706    pub fn verbosity(mut self, verbosity: Verbosity) -> Self {
707        self.verbosity = verbosity;
708        self
709    }
710
711    /// Build the `ReachSet2Options` from the builder where the validity of the options is checked.
712    #[inline]
713    pub fn build(self) -> Result<ReachSet2Options, CoppError> {
714        self.validate()?;
715        Ok(ReachSet2Options {
716            lp_feas_tol: self.lp_feas_tol,
717            a_cmp_abs_tol: self.a_cmp_abs_tol,
718            a_cmp_rel_tol: self.a_cmp_rel_tol,
719            verbosity: self.verbosity,
720        })
721    }
722
723    /// This function checks the validity of the options and returns an error if any option is invalid.
724    #[inline]
725    pub fn validate(&self) -> Result<(), CoppError> {
726        check_strictly_positive("ReachSet2OptionsBuilder", "lp_feas_tol", self.lp_feas_tol)?;
727        check_abs_rel_tol(
728            "ReachSet2OptionsBuilder",
729            "a_cmp_abs_tol",
730            self.a_cmp_abs_tol,
731            "a_cmp_rel_tol",
732            self.a_cmp_rel_tol,
733        )?;
734        Ok(())
735    }
736}
737
738/// The options for `reach_set2`.
739pub struct ReachSet2Options {
740    pub(crate) lp_feas_tol: f64,
741    pub(crate) a_cmp_abs_tol: f64,
742    pub(crate) a_cmp_rel_tol: f64,
743    pub(crate) verbosity: Verbosity,
744}
745
746impl ReachSet2Options {
747    #[inline]
748    pub fn lp_feas_tol(&self) -> f64 {
749        self.lp_feas_tol
750    }
751    #[inline]
752    pub fn a_cmp_abs_tol(&self) -> f64 {
753        self.a_cmp_abs_tol
754    }
755    #[inline]
756    pub fn a_cmp_rel_tol(&self) -> f64 {
757        self.a_cmp_rel_tol
758    }
759    #[inline]
760    pub fn verbosity(&self) -> Verbosity {
761        self.verbosity
762    }
763}
764
765#[cfg(test)]
766mod tests {
767    use super::*;
768    use crate::copp::copp2::stable::basic::Topp2ProblemBuilder;
769    use crate::copp::copp2::stable::topp2_ra::topp2_ra;
770    use crate::path::{add_symmetric_axial_limits_for_test, lissajous_path_for_test};
771    use crate::robot::robot_core::Robot;
772
773    #[test]
774    fn test_reach_set2() -> Result<(), CoppError> {
775        let dim = 7;
776        let n: usize = 1000;
777
778        let options = ReachSet2OptionsBuilder::new()
779            .lp_feas_tol(1E-9)
780            .a_cmp_abs_tol(1E-9)
781            .a_cmp_rel_tol(1E-9)
782            .verbosity(Verbosity::Summary)
783            .build()?;
784
785        let mut robot = Robot::with_capacity(dim, n);
786        let mut rng = rand::rng();
787
788        let (s, derivs, _, _) = lissajous_path_for_test(dim, n, &mut rng).map_err(|e| {
789            CoppError::InvalidInput(
790                "test_reach_set2_with_path_helper".into(),
791                format!("failed to generate test path derivatives: {e}"),
792            )
793        })?;
794
795        let dq = derivs.dq.as_ref().ok_or_else(|| {
796            CoppError::InvalidInput(
797                "test_reach_set2_with_path_helper".into(),
798                "missing dq in path derivatives".into(),
799            )
800        })?;
801        let ddq = derivs.ddq.as_ref().ok_or_else(|| {
802            CoppError::InvalidInput(
803                "test_reach_set2_with_path_helper".into(),
804                "missing ddq in path derivatives".into(),
805            )
806        })?;
807        let dddq = derivs.dddq.as_ref().ok_or_else(|| {
808            CoppError::InvalidInput(
809                "test_reach_set2_with_path_helper".into(),
810                "missing dddq in path derivatives".into(),
811            )
812        })?;
813
814        robot.with_s(&s.as_view())?;
815        robot.with_q(
816            &derivs.q.as_view(),
817            &dq.as_view(),
818            &ddq.as_view(),
819            Some(&dddq.as_view()),
820            0,
821        )?;
822
823        add_symmetric_axial_limits_for_test(&mut robot, 1.0, 1.0, Some(5.0)).map_err(|e| {
824            CoppError::InvalidInput(
825                "test_reach_set2_with_path_helper".into(),
826                format!("failed to add symmetric axial limits: {e}"),
827            )
828        })?;
829
830        let topp2_problem = Topp2ProblemBuilder::new(&robot, (0, n - 1), (0.0, 0.0)).build()?;
831
832        let reach_set_back = reach_set2_backward(&topp2_problem, &options)?;
833        let reach_set_for = reach_set2_bidirectional(&topp2_problem, &options)?;
834        let a_ra = topp2_ra(&topp2_problem, &options)?;
835
836        crate::verbosity_log!(
837            crate::diag::Verbosity::Summary,
838            "a_max_back.len() = {};",
839            reach_set_back.a_max.len()
840        );
841        crate::verbosity_log!(
842            crate::diag::Verbosity::Summary,
843            "a_max_for.len() = {};",
844            reach_set_for.a_max.len()
845        );
846        crate::verbosity_log!(
847            crate::diag::Verbosity::Summary,
848            "a_ra.len() = {};",
849            a_ra.len()
850        );
851
852        Ok(())
853    }
854}